// Copyright Epic Games, Inc. All Rights Reserved.

#pragma once

#include <zenutil/basicfile.h>

ZEN_THIRD_PARTY_INCLUDES_START
#include <spdlog/details/log_msg.h>
#include <spdlog/sinks/base_sink.h>
ZEN_THIRD_PARTY_INCLUDES_END

#include <filesystem>

namespace zen::logging {

// Basically the same functionality as spdlog::sinks::rotating_file_sink with the biggest difference
// being that it just ignores any errors when writing/rotating files and keeps chugging on.
// It will keep trying to log, and if it starts to work it will continue to log.
class RotatingFileSink : public spdlog::sinks::sink
{
public:
	RotatingFileSink(const std::filesystem::path& BaseFilename, std::size_t MaxSize, std::size_t MaxFiles, bool RotateOnOpen = false)
	: m_BaseFilename(BaseFilename)
	, m_MaxSize(MaxSize)
	, m_MaxFiles(MaxFiles)
	{
		std::error_code Ec;
		if (RotateOnOpen)
		{
			RwLock::ExclusiveLockScope RotateLock(m_Lock);
			Rotate(RotateLock, Ec);
		}
		else
		{
			m_CurrentFile.Open(m_BaseFilename, BasicFile::Mode::kWrite, Ec);
			if (!Ec)
			{
				m_CurrentSize = m_CurrentFile.FileSize(Ec);
			}
			if (!Ec)
			{
				if (m_CurrentSize > m_MaxSize)
				{
					RwLock::ExclusiveLockScope RotateLock(m_Lock);
					Rotate(RotateLock, Ec);
				}
			}
		}

		if (Ec)
		{
			throw std::system_error(Ec, fmt::format("Failed to open log file '{}'", m_BaseFilename.string()));
		}
	}

	virtual ~RotatingFileSink()
	{
		try
		{
			RwLock::ExclusiveLockScope RotateLock(m_Lock);
			m_CurrentFile.Close();
		}
		catch (const std::exception&)
		{
		}
	}

	RotatingFileSink(const RotatingFileSink&) = delete;
	RotatingFileSink(RotatingFileSink&&)	  = delete;

	RotatingFileSink& operator=(const RotatingFileSink&) = delete;
	RotatingFileSink& operator=(RotatingFileSink&&) = delete;

	virtual void log(const spdlog::details::log_msg& msg) override
	{
		try
		{
			spdlog::memory_buf_t Formatted;
			if (TrySinkIt(msg, Formatted))
			{
				return;
			}
			while (true)
			{
				{
					RwLock::ExclusiveLockScope RotateLock(m_Lock);
					// Only rotate if no-one else has rotated before us
					if (m_CurrentSize > m_MaxSize || !m_CurrentFile.IsOpen())
					{
						std::error_code Ec;
						Rotate(RotateLock, Ec);
						if (Ec)
						{
							return;
						}
					}
				}
				if (TrySinkIt(Formatted))
				{
					return;
				}
			}
		}
		catch (const std::exception&)
		{
			// Silently eat errors
		}
	}
	virtual void flush() override
	{
		try
		{
			RwLock::SharedLockScope Lock(m_Lock);
			if (m_CurrentFile.IsOpen())
			{
				m_CurrentFile.Flush();
			}
		}
		catch (const std::exception&)
		{
			// Silently eat errors
		}
	}

	virtual void set_pattern(const std::string& pattern) override
	{
		try
		{
			RwLock::ExclusiveLockScope _(m_Lock);
			m_Formatter = spdlog::details::make_unique<spdlog::pattern_formatter>(pattern);
		}
		catch (const std::exception&)
		{
			// Silently eat errors
		}
	}
	virtual void set_formatter(std::unique_ptr<spdlog::formatter> sink_formatter) override
	{
		try
		{
			RwLock::ExclusiveLockScope _(m_Lock);
			m_Formatter = std::move(sink_formatter);
		}
		catch (const std::exception&)
		{
			// Silently eat errors
		}
	}

private:
	void Rotate(RwLock::ExclusiveLockScope&, std::error_code& OutEc)
	{
		m_CurrentFile.Close();

		OutEc = RotateFiles(m_BaseFilename, m_MaxFiles);
		if (OutEc)
		{
			return;
		}

		m_CurrentFile.Open(m_BaseFilename, BasicFile::Mode::kWrite, OutEc);
		if (OutEc)
		{
			return;
		}

		// If we fail to rotate, try extending the current log file
		m_CurrentSize = m_CurrentFile.FileSize(OutEc);
	}

	bool TrySinkIt(const spdlog::details::log_msg& msg, spdlog::memory_buf_t& OutFormatted)
	{
		RwLock::SharedLockScope Lock(m_Lock);
		if (!m_CurrentFile.IsOpen())
		{
			return false;
		}
		m_Formatter->format(msg, OutFormatted);
		size_t add_size	 = OutFormatted.size();
		size_t write_pos = m_CurrentSize.fetch_add(add_size);
		if (write_pos + add_size > m_MaxSize)
		{
			return false;
		}
		std::error_code Ec;
		m_CurrentFile.Write(OutFormatted.data(), OutFormatted.size(), write_pos, Ec);
		if (Ec)
		{
			return false;
		}
		return true;
	}

	bool TrySinkIt(const spdlog::memory_buf_t& Formatted)
	{
		RwLock::SharedLockScope Lock(m_Lock);
		if (!m_CurrentFile.IsOpen())
		{
			return false;
		}
		size_t add_size	 = Formatted.size();
		size_t write_pos = m_CurrentSize.fetch_add(add_size);
		if (write_pos + add_size > m_MaxSize)
		{
			return false;
		}

		std::error_code Ec;
		m_CurrentFile.Write(Formatted.data(), Formatted.size(), write_pos, Ec);
		if (Ec)
		{
			return false;
		}
		return true;
	}

	RwLock							   m_Lock;
	const std::filesystem::path		   m_BaseFilename;
	std::unique_ptr<spdlog::formatter> m_Formatter;
	std::atomic_size_t				   m_CurrentSize;
	const std::size_t				   m_MaxSize;
	const std::size_t				   m_MaxFiles;
	BasicFile						   m_CurrentFile;
};

}  // namespace zen::logging
